Skip to content

OAuth 2.0 Protected Resource Metadata endpoint#260773

Open
elena-shostak wants to merge 17 commits intoelastic:mainfrom
elena-shostak:2750-resource-endpoint
Open

OAuth 2.0 Protected Resource Metadata endpoint#260773
elena-shostak wants to merge 17 commits intoelastic:mainfrom
elena-shostak:2750-resource-endpoint

Conversation

@elena-shostak
Copy link
Copy Markdown
Contributor

@elena-shostak elena-shostak commented Apr 1, 2026

Summary

OAuth 2.0 Protected Resource Metadata endpoint https://datatracker.ietf.org/doc/rfc9728/

xpack.security.mcp.oauth2.metadata:
  authorization_servers:
    - <server1>
    - <server2>
  resource: <resource_uri>
  bearer_methods_supported:
    - header
    - body
    - query
  scopes_supported:
    - all
  resource_documentation: <resource_documentation_uri>

How to test

  1. Add configuration to Kibana
xpack.security.mcp.oauth2.metadata:
  authorization_servers: ['https://localhost:8444/oauth2']
  resource: http://localhost:5601

Note

You may need to add self signed certificate to claude/settings.json
"NODE_EXTRA_CA_CERTS": "<path_to_kibana>/src/platform/packages/shared/kbn-dev-utils/certs/ca.crt"

  1. Start ES and Kibana with UIAM
yarn es serverless --projectType observability --uiam-oauth
yarn start --serverless=oblt --uiam
  1. Login to Kibana
  2. Register Claude as a client
curl --location 'https://localhost:8443/uiam/api/v1/oauth/clients' \
--header 'Content-Type: application/json' \
--header 'Authorization: ApiKey essu_dev_TnpKcmMyVTFkMEo2WW5scU5XUm9PVWw2TVRNNmJqSXRVbWxTVEZNeVowNVRkMWhUVWpCd1l6SjBadz09AAAAAN10T0s=' \
--data '{
    "client_name": "Claude Code",
    "resource": "http://localhost:5601/"
}'

You should get a clientId with response

{
    "id": "<your_client_id>"
    "type": "public",
    "resource": "http://localhost:5601/",
    "creation": "2026-04-01T14:56:04.184718009Z",
    "revoked": false,
    "connections": {
        "active": [],
        "revoked": []
    },
    "client_name": "Claude Code",
    "client_metadata": {}
}
  1. Add Kibana MCP to Claude
claude mcp add --transport http --client-id <your_client_id> kibana-mcp http://localhost:5601/api/agent_builder/mcp
  1. Check the connection
claud mcp list

Should output

kibana-mcp: http://localhost:5601/api/agent_builder/mcp (HTTP) - !Needs authentication
  1. Initiate Claude session and use /mcp to start OAuth flow
Screenshot 2026-04-08 at 12 02 12
Screen.Recording.2026-04-08.at.13.23.44.mov

Checklist

Closes: https://github.com/elastic/kibana-team/issues/2750
Relates: #256182

@elena-shostak elena-shostak force-pushed the 2750-resource-endpoint branch from a031945 to ff74af2 Compare April 8, 2026 10:25
Comment on lines +280 to +282
const expectedAudience = this.#kibanaServerURL.endsWith('/')
? this.#kibanaServerURL
: `${this.#kibanaServerURL}/`;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Claude's client normalizes the URL to the canonical form with an explicit path. UIAM should treat these as equivalent when comparing, but it doesn't right now, so appending trailing slash as temporary fix, otherwise we would get

Audience mismatch: expected http://localhost:5601 but token has http://localhost:5601/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave a TODO comment to record that it's a temporary workaround that we should remove.

@elena-shostak elena-shostak added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting Team:Security Platform Security: Auth, Users, Roles, Spaces, Audit Logging, etc t// Feature:Security/Authorization Platform Security - Authorization Feature:Security/Authentication Platform Security - Authentication and removed Feature:Security/Authorization Platform Security - Authorization labels Apr 8, 2026
@elena-shostak elena-shostak requested a review from azasypkin April 8, 2026 15:37
@elena-shostak elena-shostak marked this pull request as ready for review April 8, 2026 15:37
@elena-shostak elena-shostak requested review from a team as code owners April 8, 2026 15:37
@elasticmachine
Copy link
Copy Markdown
Contributor

Pinging @elastic/kibana-security (Team:Security)

@elasticmachine
Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] affected Scout: [ security / entity_store ] plugin / local-serverless-security_complete - Entity Store History Snapshot - history snapshot: copies latest to history index and resets behaviors on latest
  • [job] [logs] FTR Configs #141 / Search solution tests Search index details page Solution Nav - Search search index details page has index actions enabled add field button is enabled

Metrics [docs]

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/mock-idp-utils 62 65 +3

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
mockIdpPlugin 6.8KB 6.8KB +31.0B
Unknown metric groups

API count

id before after diff
@kbn/mock-idp-utils 70 74 +4

History

@azasypkin
Copy link
Copy Markdown
Contributor

ACK: will review today

Copy link
Copy Markdown
Contributor

@azasypkin azasypkin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested locally in Claude Desktop using the config below. The authentication flow and subsequent tool calls worked correctly. LGTM! Just a few nits and questions.

{
  "mcpServers": {
    "oauth-server": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "http://localhost:5601/api/agent_builder/mcp",
        "--static-oauth-client-info",
        "{ \"client_id\": \"XXXX\" }",
        "--static-oauth-client-metadata",
        "{ \"scope\": \"all\" }"
      ],
      "env": {
        "NODE_TLS_REJECT_UNAUTHORIZED": "0"
      }
    }
  }
}

Comment on lines +115 to +116
'--env',
'UIAM_SERVICE_BOUNDARY=external',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we don't need this for cosmos db?

Suggested change
'--env',
'UIAM_SERVICE_BOUNDARY=external',

`${KBN_CERT_PATH}:/tmp/server.crt:z`,

'-p',
`127.0.0.1:${+new URL(MOCK_IDP_UIAM_SERVICE_INTERNAL_URL)?.port + 1}:8443`, // UIAM OAuth HTTPS port
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: now that we two UAIM containers, I think it's time to remove this +new URL(url)?.port and +new URL(url)?.port + 1 logic and just define ports separately for all containers:

const ENV_DEFAULTS = {
  UIAM_COSMOS_DB_PORT: '8081',
  UIAM_COSMOS_DB_UI_PORT: '8082',
  UIAM_SERVICE_PORT: '8443',
  UIAM_OAUTH_SERVICE_PORT: '8444',
  UIAM_APP_LOGGING_LEVEL: 'DEBUG',
  UIAM_LOGGING_LEVEL: 'INFO',
};

And then just use env.UIAM_COSMOS_DB_PORT, env.UIAM_SERVICE_PORT, and env.UIAM_OAUTH_SERVICE_PORT.

What do you think?

* UIAM external (OAuth) container needs a filesystem path to IdP metadata XML.
* Override with `MOCK_IDP_KIBANA_URL`.
*/
async function extraDockerParamsForUiamOauthContainer(): Promise<string[]> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: cannot we define these params directly on UIAM_OAUTH_CONTAINER, now that we don't write a custom metadata file?

* Returns the list of UIAM containers to run.
* When `uiamOAuth` is true, includes the UIAM OAuth container.
*/
export function getUiamContainers(uiamOAuth?: boolean): UiamContainer[] {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unnamed bool params are a bit hard to read in the consumer code, what about { includeUiamOAuth: boolean } or something like this?

typeof issuerElement === 'string' ? issuerElement : issuerElement?._ ?? undefined;
return {
requestId: attrs.ID as string,
acsUrl: attrs.AssertionConsumerServiceURL as string | undefined,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional nit: if it helps to simplify the code here and in MockIdP, we can always hard code ACS as kibana URL + fixed /api/security/saml/callback as we shouldn't support anything else, but feel free to keep everything as is.

}
);

// MCP SDK (RFC 9728) tries path-aware discovery first (e.g.,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: direct link to a relevant section of the spec here would also be helpful.


which the SDK

What SDK is meant here?


{path*} vs /.well-known/oauth-protected-resource/api/agent_builder/mcp

Would it make sense to validate that if path is provided it corresponds to /api/agent_builder/mcp?

Comment on lines +386 to +388
schema.literal('header'),
schema.literal('body'),
schema.literal('query'),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: would be great to leave a code comment that explains what all these 3 values mean. Do we/UIAM support all of these?

Other fields would probably also benefit from same brief docs.

});

export type UiamConfigType = TypeOf<typeof ConfigSchema>['uiam'];
export type McpConfigType = TypeOf<typeof ConfigSchema>['mcp'];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it doesn't look we use it anywhere.

Suggested change
export type McpConfigType = TypeOf<typeof ConfigSchema>['mcp'];

: this.authenticator.getRequestOriginalURL(request);

// For routes that accept UIAM OAuth tokens, return a 401 with a WWW-Authenticate header
// containing the resource_metadata URL (RFC 9728) instead of redirecting to the login page.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's leave a direct link to a relevant part of the spec.

body: JSON.stringify({
jsonrpc: '2.0',
id: null,
error: { code: -32001, message: 'Unauthorized' },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: what does this code mean and where is it coming from?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting Feature:Security/Authentication Platform Security - Authentication release_note:skip Skip the PR/issue when compiling release notes Team:Security Platform Security: Auth, Users, Roles, Spaces, Audit Logging, etc t//

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants